﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data.Services.Client;
using System.Linq;
using System.Linq.Expressions;
using System.Net;
using System.Reflection;

namespace Elo.Framework.Data.DataAccess.DataService
{

    /// <summary>
    /// Oferece acesso e manipulação dos dados existentes na entidade com WCFDataServices.
    /// </summary>
    /// <typeparam name="TContext">Tipo da entidade de contexto.</typeparam>
    /// <typeparam name="TSource">Tipo da entidade a ser manipulada. Ex: Empresas</typeparam>
    [Serializable, Description("Oferece acesso e manipulação dos dados existentes na entidade.")]
    public class Repository<TContext, TSource> : IRepository<TSource> where TSource : class 
    {
        /// <summary>
        /// Contexto do serviço de acesso a dados.
        /// </summary>
        [Description("Contexto do serviço de acesso a dados.")]
        public DataServiceContext Context { get; set; }

        /// <summary>
        /// String de conexão para acesso a dados.
        /// </summary>
        [Description("String de conexão para acesso a dados.")]
        public String ConnectionString { get; set; }

        /// <summary>
        /// Inicializa o objeto de acesso a dados especificando o contexto de acesso a dados.
        /// </summary>
        /// <param name="connectionString">Representa o contexto do serviço de acesso a dados do ADO.Net</param>
        public Repository(string connectionString)
        {
            if (connectionString == null)
                throw new ArgumentNullException("connectionString");

            ConnectionString = connectionString;
        }

        /// <summary>
        /// Retorna um objeto da entidade manipulada conforme o valor do ID.
        /// </summary>
        /// <param name="where">Valores referente ao ID da entidade.</param>
        /// <returns>Entidade encontrada. Caso contrário retorna null.</returns>
        public virtual TSource GetByID(Expression<Func<TSource, bool>> where)
        {

            Context = (DataServiceContext)Activator.CreateInstance(typeof(TContext), new Uri(ConnectionString));
            try
            {
                Context.MergeOption = MergeOption.AppendOnly;
                var result = Context.CreateQuery<TSource>(typeof(TSource).Name).SingleOrDefault(where);
                return result;
            }
            catch (Exception ex)
            {
                if (ex.InnerException != null)
                    throw new Exception(ex.InnerException.Message, ex);
                throw new Exception(ex.Message, ex);
            }
        }

        /// <summary>
        /// Retorna um objeto da entidade manipulada conforme o valor do ID.
        /// </summary>
        /// <param name="expand">Lista de propriedades para expansão.</param>
        /// <param name="where">Valores referente ao ID da entidade.</param>
        /// <returns>Entidade encontrada. Caso contrário retorna null.</returns>
        public virtual TSource GetByID(string expand, Expression<Func<TSource, bool>> where)
        {

            Context = (DataServiceContext)Activator.CreateInstance(typeof(TContext), new Uri(ConnectionString));
            try
            {
                Context.MergeOption = MergeOption.AppendOnly;
                var result = Context.CreateQuery<TSource>(typeof(TSource).Name).Expand(expand).SingleOrDefault(where);
                return result;
            }
            catch (DataServiceQueryException ex)
            {
                if (ex.Response.StatusCode == (int)HttpStatusCode.NotFound)
                    return default(TSource);
                throw new Exception(ex.Message, ex);
            }
            catch (Exception ex)
            {
                if (ex.InnerException != null)
                    throw new Exception(ex.InnerException.Message, ex);
                throw new Exception(ex.Message, ex);
            }
        }

        /// <summary>
        /// Retorna todos objetos da entidade manipulada.
        /// </summary>
        /// <returns>Retorna uma lista dos objetos.</returns>
        public virtual IList<TSource> GetAll()
        {
            Context = (DataServiceContext)Activator.CreateInstance(typeof(TContext), new Uri(ConnectionString));
            try
            {
                Context.MergeOption = MergeOption.AppendOnly;
                IList<TSource> result = Context.CreateQuery<TSource>(typeof(TSource).Name).ToList();
                return result;
            }
            catch (Exception ex)
            {
                if (ex.InnerException != null)
                    throw new Exception(ex.InnerException.Message, ex);
                throw new Exception(ex.Message, ex);
            }
        }

        /// <summary>
        /// Retorna todos objetos da entidade manipulada.
        /// </summary>
        /// <param name="expand">Lista de propriedades para expansão.</param>
        /// <returns>Retorna uma lista dos objetos.</returns>
        public virtual IList<TSource> GetAll(string expand)
        {
            Context = (DataServiceContext)Activator.CreateInstance(typeof(TContext), new Uri(ConnectionString));
            try
            {
                Context.MergeOption = MergeOption.AppendOnly;
                IList<TSource> result = Context.CreateQuery<TSource>(typeof(TSource).Name)
                    .Expand(expand).ToList();
                return result;
            }
            catch (Exception ex)
            {
                if (ex.InnerException != null)
                    throw new Exception(ex.InnerException.Message, ex);
                throw new Exception(ex.Message, ex);
            }
        }

        /// <summary>
        /// Retorna todos objetos da entidade manipulada.
        /// </summary>
        /// <param name="expand">Lista de propriedades para expansão.</param>
        /// <returns>Retorna uma lista dos objetos.</returns>
        public virtual IList<TSource> GetAll(params Expression<Func<TSource, object>>[] expand)
        {
            Context = (DataServiceContext)Activator.CreateInstance(typeof(TContext), new Uri(ConnectionString));
            try
            {

                if (expand.Length == 0)
                    throw new Exception("Número de parametros inválido.");

                var expandString = expand.Aggregate("", (current, item) => current + (item.Body.Type.Name + "/"));

                //removenda a barra final
                expandString = expandString.Remove(expandString.LastIndexOf('/'), 1);

                Context.MergeOption = MergeOption.AppendOnly;
                IList<TSource> result = Context.CreateQuery<TSource>(typeof(TSource).Name)
                    .Expand(expandString).ToList();
                return result;
            }
            catch (Exception ex)
            {
                if (ex.InnerException != null)
                    throw new Exception(ex.InnerException.Message, ex);
                throw new Exception(ex.Message, ex);
            }
        }

        /// <summary>
        /// Retorna os objetos da entidade manipulada de acordo a condição informada.
        /// </summary>
        /// <param name="predicate">Representa a condição a ser testada pela entidade</param>
        /// <returns>Retorna uma lista dos objetos</returns>
        public virtual IList<TSource> GetWhere(Expression<Func<TSource, bool>> predicate)
        {
            Context = (DataServiceContext)Activator.CreateInstance(typeof(TContext), new Uri(ConnectionString));
            try
            {
                Context.MergeOption = MergeOption.AppendOnly;
                IList<TSource> result = Context.CreateQuery<TSource>(typeof(TSource).Name).Where(predicate).ToList();
                return result;
            }
            catch (Exception ex)
            {
                if (ex.InnerException != null)
                    throw new Exception(ex.InnerException.Message, ex);
                throw new Exception(ex.Message, ex);
            }
        }

        /// <summary>
        /// Retorna os objetos da entidade manipulada de acordo a condição informada.
        /// </summary>
        /// <param name="expand">Lista de propriedades para expansão.</param>
        /// <param name="predicate">Representa a condição a ser testada pela entidade</param>
        /// <returns>Retorna uma lista dos objetos</returns>
        public virtual IList<TSource> GetWhere(string expand, Expression<Func<TSource, bool>> predicate)
        {
            Context = (DataServiceContext)Activator.CreateInstance(typeof(TContext), new Uri(ConnectionString));
            try
            {
                Context.MergeOption = MergeOption.AppendOnly;
                IList<TSource> result = Context.CreateQuery<TSource>(typeof(TSource).Name)
                    .Expand(expand).Where(predicate).ToList();
                return result;
            }
            catch (Exception ex)
            {
                if (ex.InnerException != null)
                    throw new Exception(ex.InnerException.Message, ex);
                throw new Exception(ex.Message, ex);
            }
        }

        /// <summary>
        /// Cria um vínculo entre o objeto e suas propriedades.
        /// </summary>
        /// <param name="entity">Representa o objeto.</param>
        internal void LinkDependents(Object entity)
        {
            IList<PropertyInfo> contextProperties = Context.GetType()
                    .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly).ToList();
            IList<PropertyInfo> entityProperties = entity.GetType()
                .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly).ToList();

            foreach (var property in entityProperties)
            {
                var property1 = property;
                if (contextProperties.Where(p => p.Name.Equals(property1.PropertyType.Name)).Count() > 0)
                {
                    var propertyValue = property.GetValue(entity, null);
                    if (!(propertyValue is IEnumerable))
                    {
                        if (propertyValue != null)
                        {
                            if (Context.Entities.Where(temp => temp.Entity.Equals(propertyValue)).Count() < 1)
                                Context.AttachTo(property.PropertyType.Name, propertyValue);
                            Context.SetLink(entity, property.Name, propertyValue);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Adiciona a entidade indicada.
        /// </summary>
        /// <param name="entity">Entidade a ser incluída.</param>
        /// <returns>Retorna a entidade após a inclusão. Apresentando algum erro retorna null.</returns>
        public virtual TSource Add(TSource entity)
        {
            //Aguardando definir como será realizado a validação das entidades
            //if (!IsValid(entity))
            //    return default(TSource);

            Context = (DataServiceContext)Activator.CreateInstance(typeof(TContext), new Uri(ConnectionString));
            try
            {
                Context.MergeOption = MergeOption.AppendOnly;
                Context.AddObject(entity.GetType().Name, entity);
                LinkDependents(entity);

                Context.SaveChanges();
                return entity;
            }
            catch (Exception ex)
            {
                if (ex.InnerException != null)
                    throw new Exception(ex.InnerException.Message, ex);
                throw new Exception(ex.Message, ex);
            }
        }

        /// <summary>
        /// Exclui a entidade indicada.
        /// </summary>
        /// <param name="entity">Entidade a ser deletada.</param>
        /// <returns>Retorna true se a entidade for excluido. Caso contrário retorna false.</returns>
        public virtual bool Delete(TSource entity)
        {
            Context = (DataServiceContext)Activator.CreateInstance(typeof(TContext), new Uri(ConnectionString));
            try
            {
                Context.MergeOption = MergeOption.AppendOnly;
                Context.AttachTo(entity.GetType().Name, entity);
                Context.DeleteObject(entity);
                Context.SaveChanges();
                return true;
            }
            catch (Exception ex)
            {
                if (ex.InnerException != null)
                    throw new Exception(ex.InnerException.Message, ex);
                throw new Exception(ex.Message, ex);
            }
        }

        /// <summary>
        /// Atualiza a entidade indicada.
        /// </summary>
        /// <param name="entity">Entidade a ser atualizada.</param>
        /// <returns>Retorna true se a entidade for atualizado. Caso contrário retorna false.</returns>
        public virtual bool Update(TSource entity)
        {
            //Aguardando definir como será realizado a validação das entidades
            //if (!IsValid(entity))
            //    return false;

            Context = (DataServiceContext)Activator.CreateInstance(typeof(TContext), new Uri(ConnectionString));
            try
            {
                Context.AttachTo(entity.GetType().Name, entity);
                Context.UpdateObject(entity);
                LinkDependents(entity);

                Context.SaveChanges();
                return true;
            }
            catch (Exception ex)
            {
                if (ex.InnerException != null)
                    throw new Exception(ex.InnerException.Message, ex);
                throw new Exception(ex.Message, ex);
            }
        }

        /// <summary>
        /// Confere se os valores da entidade são válidos.
        /// </summary>
        /// <param name="entity">Entidade para conferência.</param>
        /// <returns>Retorna true se a entidade for válida. Caso contrário retorna false.</returns>
        public virtual bool IsValid(TSource entity)
        {
            return true;
        }

        /// <summary>
        /// Adiciona ao objeto e seus registros dependentes.
        /// </summary>
        /// <param name="entity">Representa o objeto.</param>
        internal void AddDependents(Object entity)
        {
            IList<PropertyInfo> contextProperties = Context.GetType()
                    .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly).ToList();
            IList<PropertyInfo> entityProperties = entity.GetType()
                .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly).ToList();

            foreach (var property in entityProperties)
            {
                var propertyType = property.PropertyType.Name;
                if (property.PropertyType.IsGenericType)
                    propertyType = property.PropertyType.GetGenericArguments()[0].Name;

                if (contextProperties.Where(p => p.Name.Equals(propertyType)).Count() > 0)
                {
                    var propertyValue = property.GetValue(entity, null);
                    if ((propertyValue is IList))
                    {
                        foreach (var current in (propertyValue as IList))
                        {
                            Context.AddObject(propertyType, current);
                            AddDependents(current);
                        }
                    }
                    else if (propertyValue != null)
                    {
                        if (Context.Entities.Where(temp => temp.Entity.Equals(propertyValue)).Count() < 1)
                            Context.AttachTo(propertyType, propertyValue);
                        Context.SetLink(entity, property.Name, propertyValue);
                    }
                }
            }
        }

        /// <summary>
        /// Exclui o objeto e seus registros dependentes.
        /// </summary>
        /// <param name="entity">Representa o objeto.</param>
        internal void DeleteDependents(Object entity)
        {
            IList<PropertyInfo> contextProperties = Context.GetType()
                    .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly).ToList();
            IList<PropertyInfo> entityProperties = entity.GetType()
                .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly).ToList();

            foreach (var property in entityProperties)
            {
                var propertyType = property.PropertyType.Name;
                if (property.PropertyType.IsGenericType)
                    propertyType = property.PropertyType.GetGenericArguments()[0].Name;

                if (contextProperties.Where(p => p.Name.Equals(propertyType)).Count() > 0)
                {
                    var propertyValue = property.GetValue(entity, null);
                    if ((propertyValue is IList))
                    {
                        foreach (var current in (propertyValue as IList))
                        {
                            DeleteDependents(current);
                            var current1 = current;
                            if (Context.Entities.Where(temp => temp.Entity.Equals(current1)).Count() < 1)
                                Context.AttachTo(propertyType, current);
                            Context.DeleteObject(current);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Adiciona a entidade indicada e seus dependentes.
        /// </summary>
        /// <param name="entity">Entidade a ser incluída.</param>
        /// <returns>Retorna a entidade após a inclusão. Apresentando algum erro retorna null.</returns>
        public virtual TSource AddWithDependents(TSource entity)
        {
            if (!IsValid(entity))
                return default(TSource);
            try
            {
                Context.MergeOption = MergeOption.AppendOnly;
                if (Context.Entities.Where(temp => temp.Entity.Equals(entity)).Count() < 1)
                    Context.AddObject(entity.GetType().Name, entity);
                AddDependents(entity);

                Context.SaveChanges();
                return entity;
            }
            catch (Exception ex)
            {
                if (ex.InnerException != null)
                    throw new Exception(ex.InnerException.Message, ex);
                throw new Exception(ex.Message, ex);
            }
        }

        /// <summary>
        /// Exclui a entidade indicada e os registros dependetes.
        /// </summary>
        /// <param name="entity">Entidade a ser deletada.</param>
        /// <returns>Retorna true se a entidade for excluido. Caso contrário retorna false.</returns>
        public virtual bool DeleteWithDependents(TSource entity)
        {
            try
            {
                Context.MergeOption = MergeOption.AppendOnly;
                if (Context.Entities.Where(temp => temp.Entity.Equals(entity)).Count() < 1)
                    Context.AttachTo(entity.GetType().Name, entity);
                DeleteDependents(entity);
                Context.DeleteObject(entity);
                Context.SaveChanges();
                return true;
            }
            catch (Exception ex)
            {
                if (ex.InnerException != null)
                    throw new Exception(ex.InnerException.Message, ex);
                throw new Exception(ex.Message, ex);
            }
        }

        // Função será implementada em versões futuras
        ///// <summary>
        ///// Atualiza o objeto e seus registros dependentes.
        ///// </summary>
        ///// <param name="entity">Representa o objeto.</param>
        //private void UpdateDependents(Object entity)
        //{
        //    IList<PropertyInfo> contextProperties = Context.GetType()
        //            .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly).ToList();
        //    IList<PropertyInfo> entityProperties = entity.GetType()
        //        .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly).ToList();

        //    foreach (PropertyInfo property in entityProperties)
        //    {
        //        string propertyType = property.PropertyType.Name;
        //        if (property.PropertyType.IsGenericType)
        //            propertyType = property.PropertyType.GetGenericArguments()[0].Name;

        //        if (contextProperties.Where(p => p.Name.Equals(propertyType)).Count() > 0)
        //        {
        //            Object propertyValue = property.GetValue(entity, null);
        //            if ((propertyValue is IList))
        //            {
        //                foreach (var current in (propertyValue as IList))
        //                {
        //                    if (Context.Entities.Where(temp => temp.Entity.Equals(current)).Count() < 1)
        //                        Context.AttachTo(propertyType, current);
        //                    Context.UpdateObject(current);
        //                    UpdateDependents(current);
        //                }
        //            }
        //            if (propertyValue != null)
        //            {
        //                if (Context.Entities.Where(temp => temp.Entity.Equals(propertyValue)).Count() < 1)
        //                    Context.AttachTo(property.PropertyType.Name, propertyValue);
        //                Context.SetLink(entity, property.Name, propertyValue);
        //            }
        //        }
        //    }
        //}

        // Função será implementada em versões futuras
        ///// <summary>
        ///// Atualiza a entidade indicada e seus dependentes.
        ///// </summary>
        ///// <param name="entity">Entidade a ser atualizada.</param>
        ///// <returns>Retorna true se a entidade for atualizado. Caso contrário retorna false.</returns>
        //public virtual bool UpdateWithDependents(TSource entity)
        //{
        //    if (!IsValid(entity))
        //        return false;
        //    try
        //    {
        //        if (Context.Entities.Where(temp => temp.Entity.Equals(entity)).Count() < 1)
        //            Context.AttachTo(entity.GetType().Name, entity);
        //        Context.UpdateObject(entity);
        //        UpdateDependents(entity);

        //        Context.SaveChanges();
        //        return true;
        //    }
        //    catch (Exception ex)
        //    {
        //        if (ex.InnerException != null)
        //            throw new Exception(ex.InnerException.Message, ex);
        //        else
        //            throw new Exception(ex.Message, ex);
        //    }
        //}
    }

}
